﻿using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.CodeDom.Compiler;
using System.CodeDom;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading;
using System.Reflection;
using System.Xml;
using System.Xml.Schema;

namespace Facebook.Api.Generator
{
    /// <summary>A static class that contains logic for generating the .NET Facebook API wrapper.</summary>
    public static class FacebookApiGenerator
    {
        /// <summary>Loads content from the specified url.</summary>
        /// <param name="url">The url to load content from.</param>
        /// <returns>A <see cref="String" /> value containing the content of the specified url.</returns>
        internal static String GetContentFromUri(String url)
        {
            return CachedWebContent.Load(url).Content;
        }

        /// <summary>Loads content from the specified file on the file system.</summary>
        /// <param name="path">The path of the file to load.</param>
        /// <returns>A <see cref="String" /> value containing the content of the specified file.</returns>
        internal static String GetContentFromFile(String path)
        {
            using (var stream = File.OpenRead(path))
            {
                using (var reader = new StreamReader(stream))
                {
                    return reader.ReadToEnd();
                }
            }
        }

        /// <summary>Loads content from the specified file or url.</summary>
        /// <param name="pathOrUrl">The path or url of the file to load.</param>
        /// <returns>A <see cref="String" /> value containing the content of the specified file or url.</returns>
        internal static String GetContent(String pathOrUrl)
        {
            if (Uri.IsWellFormedUriString(pathOrUrl, UriKind.Absolute)) return FacebookApiGenerator.GetContentFromUri(pathOrUrl);
            else if (File.Exists(pathOrUrl)) return FacebookApiGenerator.GetContentFromFile(pathOrUrl);
            else return null;
        }

        /// <summary>Generates the .NET Facebook API wrapper project.</summary>
        /// <param name="schemaPathOrUrl">The path or url of the Facebook API XML schema to use.</param>
        /// <param name="methodSignaturePathOrUrl">The path or url of the document containing method signature definitions.</param>
        /// <param name="language">The .NET language to generate the project in.</param>
        /// <param name="outputPath">The directory path to write the project to.</param>
        public static void Generate(String language, String outputPath)
        {
            var compileUnits = new List<CodeCompileUnit>();

            // create the "auto-generated" warning that will appear at the top of each file
            var assemblyName = new AssemblyName(Assembly.GetExecutingAssembly().FullName);
            var versionComponents = 2 +
                ((assemblyName.Version.Build > 0 || assemblyName.Version.Revision > 0) ? 1 : 0) +
                (assemblyName.Version.Revision > 0 ? 1 : 0);
            var generatedComment = new CodeCommentStatement(String.Format(Content.AutoGenerated, assemblyName.Version.ToString(versionComponents), ApiSourcePaths.ObjectSchema, ApiSourcePaths.WikiHome));

            // load schema content from the specified document and parse out the types.
            var schemaContent = FacebookApiGenerator.GetContent(ApiSourcePaths.ObjectSchema);
            using (var reader = new StringReader(schemaContent))
            {
                var parser = new FacebookApiSchemaParser(FacebookApiGenerator.GenerateMethodSignatures(), generatedComment);
                compileUnits.AddRange(parser.Parse(reader));
            }

            compileUnits.Add(FacebookApiGenerator.GenerateErrorCodesEnum());

            var outputBaseDir = new DirectoryInfo(outputPath);
            if (!outputBaseDir.Exists) outputBaseDir.Create();

            var baseNamespaceQuery =
                from compileUnit in compileUnits
                from ns in compileUnit.Namespaces.OfType<CodeNamespace>()
                orderby ns.Name.Split('.').Length
                select ns.Name;

            String baseNamespace = baseNamespaceQuery.Distinct().First();

            var generatorOptions = new CodeGeneratorOptions();

            var projectFile = new VsProjectFile(Path.Combine(outputBaseDir.FullName, "Facebook.csproj"));
            projectFile.RemoveGeneratedFileReferences();

            using (var provider = CodeDomProvider.CreateProvider(language))
            {
                var toCompile = new List<CompileItem>();
                
                foreach (var compileUnit in compileUnits)
                {
                    var ns = compileUnit.Namespaces[0].Name;
                    var namespaceRelativePath = ns.Replace(baseNamespace, String.Empty).Trim('.').Replace('.', '\\');
                    var namespacePath = Path.Combine(outputBaseDir.FullName, namespaceRelativePath);
                    var codeType = compileUnit.Namespaces[0].Types[0];
                    var fileName = codeType.Name + ".api.g." + provider.FileExtension;

                    String dependentFile;
                    var mergeType = projectFile.GetMergeType(Path.Combine(namespaceRelativePath, codeType.Name), out dependentFile);

                    if (mergeType == GeneratedMergeType.DoNotGenerate)
                    {
                        continue;
                    }
                    else if (mergeType == GeneratedMergeType.GeneratePartial)
                    {
                        codeType.IsPartial = true;
                    }

                    if (codeType.Name.Contains("Context") || codeType.Members.OfType<CodeTypeMember>().Any(member => member.GetType() != typeof(CodeConstructor)))
                    {
                        projectFile.AddFileReference(Path.Combine(namespaceRelativePath, fileName), dependentFile);
                        toCompile.Add(new CompileItem { CompileUnit = compileUnit, FileName = fileName, NamespacePath = namespacePath });
                    }
                }

                outputBaseDir.GetFiles("*.g.*", SearchOption.AllDirectories).Where(file => !file.FullName.Contains("svn")).ToList().ForEach(file => file.Delete());
                foreach (var compileItem in toCompile)
                {
                    if (!Directory.Exists(compileItem.NamespacePath)) Directory.CreateDirectory(compileItem.NamespacePath);

                    using (var file = File.Open(Path.Combine(compileItem.NamespacePath, compileItem.FileName), FileMode.Create))
                    {
                        using (var writer = new StreamWriter(file))
                        {
                            provider.GenerateCodeFromCompileUnit(compileItem.CompileUnit, writer, generatorOptions);                            
                        }
                    }
                }

                projectFile.Save();
            }
        }

        public static Dictionary<String, String> GetPageContent(IEnumerable<String> methodNames)
        {
            DateTime start = DateTime.Now;
            var dict = new Dictionary<String, String>();
            var queue = new Queue<String>(methodNames);
            var threads = new List<Thread>(30);

            while (queue.Count > 0)
            {
                while (threads.Count == threads.Capacity)
                {
                    if (threads.RemoveAll(t => (t.ThreadState & ThreadState.Background) != ThreadState.Background) == 0) Thread.Sleep(0);
                }
                var methodName = queue.Dequeue();

                var thread = new Thread(state =>
                {
                    var pageUrl = String.Format(ApiSourcePaths.MethodPageFormat, methodName);
                    var methodPageContent = FacebookApiGenerator.GetContent(pageUrl);
                    dict.Add(methodName, methodPageContent);
                    var thisThread = (Thread)state;
                    if (threads.Contains(thisThread)) threads.Remove((Thread)state);
                });
                thread.IsBackground = true;
                thread.Priority = ThreadPriority.BelowNormal;
                threads.Add(thread);
                thread.Start(thread);
            }

            while (threads.Count > 0)
            {
                if (threads.RemoveAll(t => t == null || (t.ThreadState & ThreadState.Background) != ThreadState.Background) == 0) Thread.Sleep(0);
            }
            var timeTaken = DateTime.Now - start;
            return dict;
        }
        
        public static Dictionary<String, MethodDefinition> GenerateMethodSignatures()
        {
            var content = FacebookApiGenerator.GetContent(ApiSourcePaths.WikiHome);

            var matches = Regex.Matches(content, RegexPatterns.MethodIndex).OfType<Match>();
            var contentDict = GetPageContent(matches.Select(match => match.Groups["name"].Value));

            var result =
                from match in matches
                select new MethodDefinition
                {
                    Name = match.Groups["name"].Value,
                    Description = match.Groups["description"].Value,
                    Parameters = GenerateParameterDeclarations(match.Groups["name"].Value, match.Groups["param"].Captures.OfType<Capture>().Select(cap => cap.Value).ToArray(), contentDict)
                };
            
            return result.ToDictionary(method => method.Name, method => method);
        }

        public static CodeParameterDeclarationExpression[] GenerateParameterDeclarations(String methodName, String[] paramNames, Dictionary<String, String> contentDict)
        {
            var commonParams = new String[] { "api_key", "call_id", "sig", "session_key", "v", "format", "callback" };

            var content = contentDict[methodName];
            var contentStart = content.IndexOf("<a name=\"Parameters\"></a>");
            var contentEnd = content.IndexOf("</table>", contentStart);
            content = content.Substring(contentStart, contentEnd - contentStart);

            var match = Regex.Match(content, RegexPatterns.MethodPage);
            var capsReq = match.Groups["required"].Captures.OfType<Capture>().ToList();
            var capsName = match.Groups["paramName"].Captures.OfType<Capture>().ToList();
            var capsType = match.Groups["paramType"].Captures.OfType<Capture>().ToList();

            var indexes = new List<Int32>(capsReq.Count);
            for (var i = 0; i < indexes.Capacity; i++) indexes.Add(i);
                        
            var regexResult =
                from capReq in capsReq
                join capName in capsName on capsReq.IndexOf(capReq) equals capsName.IndexOf(capName)
                join capType in capsType on capsReq.IndexOf(capReq) equals capsType.IndexOf(capType)
                where !commonParams.Contains(capName.Value)
                select new
                {
                    IsRequired = capReq.Index < (match.Groups["required"].Captures.OfType<Capture>().Where(cap => cap.Value == "optional").Select(cap => (Int32?)cap.Index).FirstOrDefault() ?? Int32.MaxValue),
                    ParamName = capName.Value,
                    ParamType = GetMethodTypeName(capType.Value)
                };

            var result =
                from param in regexResult
                select GenerateParameterDeclaration(param.ParamType, param.ParamName, param.IsRequired);

            return result.ToArray();
        }

        public static String GetMethodTypeName(String apiTypeName)
        {
            Type type = null;
            try { type = ((XmlTypeCode)Enum.Parse(typeof(XmlTypeCode), apiTypeName.ToPascalCase())).ToType(); }
            catch { }

            if (type == null)
            {
                switch (apiTypeName)
                {
                    case "json array":
                    case "array": return "String[]";

                    case "bool": return "Boolean";

                    case "markup": return "String";

                    case "uid": return "Int64";
                }
                throw new ArgumentException(String.Format("Unrecognized API type: {0}", apiTypeName), "apiTypeName");
            }
            else return type.Name;
        }

        public static CodeParameterDeclarationExpression GenerateParameterDeclaration(String type, String name, Boolean isRequired)
        {
            var dec = new CodeParameterDeclarationExpression(type, name.ToCamelCase());

            var paramAttribute = new CodeAttributeDeclaration("FacebookMethodParameterAttribute",
                new CodeAttributeArgument(new CodeSnippetExpression("\"" + name + "\"")));

            if (isRequired) paramAttribute.Arguments.Add(new CodeAttributeArgument(
                 new CodeBinaryOperatorExpression(
                     new CodeFieldReferenceExpression(new CodeTypeReferenceExpression("RequiredParameterFlags"), "RequiredDesktop"),
                     CodeBinaryOperatorType.BitwiseOr,
                     new CodeFieldReferenceExpression(new CodeTypeReferenceExpression("RequiredParameterFlags"), "RequiredWebsite"))));

            dec.CustomAttributes.Add(paramAttribute);            

            return dec;
        }

        public static CodeCompileUnit GenerateErrorCodesEnum()
        {
            var content = FacebookApiGenerator.GetContent(ApiSourcePaths.ErrorCodes);

            var matches = Regex.Matches(content, @"(?s)<tr>\s*<td>\s*(?<code>\d+)\s*</td>\s*<td>\s*API_EC_(?<name>\w+)\s*</td>\s*<td>\s*(?<desc>.*?)\s*</td>\s*(?:<td>.*?</td>\s*)?</tr>");

            var members =
                from match in matches.OfType<Match>()
                select GenerateErrorCodeField(match.Groups["code"].Value, match.Groups["name"].Value.ToLower(), match.Groups["desc"].Value);

            var compileUnit = new CodeCompileUnit();
            var ns = new CodeNamespace("Facebook.Api");
            var codeType = new CodeTypeDeclaration("ErrorCode") { Attributes = MemberAttributes.Public, IsEnum = true };
            codeType.BaseTypes.Add(typeof(short));
            codeType.Members.AddRange(members.ToArray());

            ns.Types.Add(codeType);
            compileUnit.Namespaces.Add(ns);

            return compileUnit;
        }

        public static CodeMemberField GenerateErrorCodeField(String code, String name, String desc)
        {
            var field = new CodeMemberField(String.Empty, name.ToPascalCase());            
            field.InitExpression = new CodeSnippetExpression(code);
            field.Comments.Add(new CodeCommentStatement(String.Format("<summary>{0}</summary>", desc), true));

            return field;
        }
    }
}